package de.dailab.graphworld.bean;

import de.dailab.graphworld.bean.utils.BeanUtils;
import de.dailab.graphworld.bean.utils.RoutePlanner;
import de.dailab.graphworld.ontology.ContestGroup;
import de.dailab.graphworld.ontology.ContestProtocol;
import de.dailab.graphworld.ontology.Edge;
import de.dailab.graphworld.ontology.Message;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.ReplyType;
import de.dailab.graphworld.ontology.Task;
import de.dailab.graphworld.ontology.exceptions.MoveException;
import de.dailab.jiactng.agentcore.action.ActionResult;
import de.dailab.jiactng.agentcore.comm.ICommunicationAddress;
import de.dailab.jiactng.agentcore.comm.IGroupAddress;
import de.dailab.jiactng.agentcore.comm.message.JiacMessage;
import de.dailab.jiactng.agentcore.knowledge.IFact;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 *
 * @author Kseniya
 */
public abstract class ContestMember extends BeanPhysical {

    protected final List<Task> jobs = new LinkedList<>();
    private final List<Task> jobsInProgress = new LinkedList<>();
    protected final Queue<ActionContainer> taskActions = new LinkedList<>();
    private RoutePlanner planner;
    protected final Queue<Node> route = new LinkedList<>();

    @Override
    protected Collection<IGroupAddress> getGroups() {
        return Collections.singleton(getCnpGroup());
    }

    protected final IGroupAddress getCnpGroup() {
        return ContestGroup.getInstance().getAddress();
    }

    protected final void sendMsgGroup(IFact payload) {
        sendMsgGroup(payload, getCnpGroup());
    }

    @Override
    protected void initialise() {
        super.initialise();
        route.clear();
        route.addAll(getPlanner().route());
    }

    @Override
    public void receiveResult(ActionResult result) {
        super.receiveResult(result);
        if (result.getFailure() == null && !taskActions.isEmpty()) {
            taskActions.poll();
        }
    }

    @Override
    public void execute() {
        super.execute();
        if (!isInitialised()) {
            return;
        }
        doExecute();
    }

    protected void doExecute() {
        // if we are already executing a task, do the next action planned
        if (!taskActions.isEmpty()) {
            taskActions.element().perform();
            return;
        }
        // we are idle -- select and start executing a new task
        planNextTask();
    }

    @Override
    public void consume(JiacMessage msg) {
        IFact payload = msg.getPayload();
        if (payload instanceof Message) {
            Message reply = (Message) payload;
            consumeMessage(reply, msg.getSender());
        }
    }

    private void consumeMessage(Message msg, ICommunicationAddress sender) {
        // no communication until we are initialised
        if (!isInitialised()) {
            return;
        }
        ReplyType type = msg.getType();
        if (ContestProtocol.INFORM_TASKS_UPDATE.equals(type)) {
            Object data = msg.getData();
            if (data instanceof Collection) {
                List tasks = new ArrayList((Collection) data);
                if (!tasks.isEmpty() && tasks.iterator().next() instanceof Task) {
                    Set<Node> taskNodes = new HashSet<>();
                    for (Object task : tasks) {
                        taskNodes.add(((Task) task).goldNode);
                    }
                    updateJobs(taskNodes);
                }
            }
        } else if (ContestProtocol.INFORM_TASK_EXECUTED.equals(type)) {
            Object data = msg.getData();
            if (data instanceof Task) {
                // clean up pending jobs
                synchronized (jobsInProgress) {
                    BeanUtils.removeJobsByNode(jobsInProgress, (Task) data);
                }
            }
        }
    }

    /**
     * Plans the actions sequence to perform all the queued tasks.
     */
    protected void planNextTask() {
        // if carrying gold and all tasks completed, go drop that gold!
        if (getGoldAmountCarrying() == getMaxCapacity()) {
            planPathToHome();
            return;
        }
        // do nothing if no jobs available
        if (jobs.isEmpty()) {
            return;
        }
        // start with the nearest task
        Task best = selectNearestTask(jobs, storedPos);
        taskActions.clear();
        taskActions.addAll(planActions(best, storedPos));
    }

    /**
     * Plans actions to perform the given task from the given node.
     *
     * @param task to be performed
     * @param from initial position
     * @return action sequence to execute
     */
    protected List<ActionContainer> planActions(Task task, Node from) {
        // calc path to the task's gold node first
        List<Edge> pathToTask;
        if (from.getID() == task.goldNode.getID()) {
            pathToTask = Collections.emptyList();
        } else {
            pathToTask = findPath(from.getID(), task.goldNode.getID());
        }
        // enqueue the needed actions
        List<ActionContainer> actions = new ArrayList<>();
        for (Edge edge : pathToTask) {
            actions.add(new ActionContainer(actionMove, new Serializable[]{edge.getTarget()}));
        }
        actions.add(new ActionContainer(actionTakeGold, null));
        return actions;
    }

    /**
     * Plans the move actions to the home node and one drop action to get rid of
     * the gold.
     */
    protected void planPathToHome() {
        List<Edge> pathToHome;
        if (storedPos.getID() == 0) {
            pathToHome = Collections.emptyList();
        } else {
            pathToHome = findPath(storedPos.getID(), 0);
            log.error("path home: " + pathToHome);
        }
        List<ActionContainer> actions = new ArrayList<>();
        for (Edge edge : pathToHome) {
            actions.add(new ActionContainer(actionMove, new Serializable[]{edge.getTarget()}));
        }
        actions.add(new ActionContainer(actionDropGold, null));
        taskActions.clear();
        taskActions.addAll(actions);
    }

    /**
     * updates tasks based on passed nodes information. Removes outdated tasks,
     * creates new tasks, updates existing ones with new gold amounts and
     * resorts tasks so the most important ones (with most gold) are delegated
     * first.
     *
     * @param nodes new nodes information
     */
    private void updateJobs(Collection<Node> nodes) {
        List<Node> nodesWithNoJob = new ArrayList<>(nodes.size());
        synchronized (jobs) {
            for (Node node : nodes) {
                boolean taskFound = false;
                Iterator<Task> itr = jobs.iterator();
                while (itr.hasNext()) {
                    Task nextTask = itr.next();
                    // a task referring that node was found, must be (eventually) updated
                    if (nextTask.goldNode.getID() == node.getID()) {
                        taskFound = true;
                        // if no gold on node at all, the task should not be delegated anymore
                        if (node.getGold() == 0) {
                            itr.remove();
                        } else {
                            // update the gold amount in task
                            nextTask.goldNode = node;
                        }
                    }
                }
                // no task found, but there is gold on node
                if (!taskFound && node.getGold() > 0) {
                    nodesWithNoJob.add(node);
                }
            }
        }
        // now check if task with this node is already in progress
        synchronized (jobsInProgress) {
            Iterator<Node> itr = nodesWithNoJob.iterator();
            while (itr.hasNext()) {
                Node nextNode = itr.next();
                for (Task job : jobsInProgress) {
                    if (job.goldNode.getID() == nextNode.getID()) {
                        itr.remove();
                        break;
                    }
                }
            }
        }
        synchronized (jobs) {
            for (Node node : nodesWithNoJob) {
                // store the task to delegate it later
                // sort by gold amount to shift more important tasks to the front
                jobs.add(new Task(node));
            }
        }
    }

    @Override
    protected void consumeExplore(Set<Node> explored) {
        super.consumeExplore(explored);
        updateJobs(explored);
        sendMsgGroup(new Message<>(jobs, ContestProtocol.INFORM_TASKS_UPDATE));
    }

    @Override
    protected void consumeTakeGold(int goldTaken) {
        super.consumeTakeGold(goldTaken);
        Collection<Task> completedTasks = BeanUtils.findTasksByNode(jobs, storedPos);
        ReplyType msgType = ContestProtocol.INFORM_TASK_EXECUTED;
        // inform others about task finished and forget about it
        for (Task task : completedTasks) {
            sendMsgGroup(new Message<>(task, msgType));
        }
        // remove finished task
        jobs.removeAll(completedTasks);
        // select and start executing next job
        planNextTask();
        performNextAction();
    }

    @Override
    protected void consumeDropGold() {
        super.consumeDropGold();
        // select and start executing next job
        planNextTask();
        performNextAction();
    }

    @Override
    protected void consumeMove(Node movedTo) {
        super.consumeMove(movedTo);
        if (canPerformNextActionImmediately()) {
            performNextAction();
        }
    }

    protected boolean canPerformNextActionImmediately() {
        // take and drop gold can be performed immediately!
        return !taskActions.isEmpty()
                && (taskActions.element().action == actionTakeGold
                || taskActions.element().action == actionDropGold);
    }

    protected final void performNextAction() {
        taskActions.element().perform();
    }

    @Override
    protected void handleNoGoldCarriedFailure(Node position) {
        setGoldAmountCarrying(0);
        storedPos = position;
        planNextTask();
        taskActions.iterator().next().perform();
    }

    @Override
    protected void handleCapacityExceeded(Node position) {
        setGoldAmountCarrying(getMaxCapacity());
        storedPos = position;
        planPathToHome();
        taskActions.iterator().next().perform();
    }

    @Override
    protected void handleInvalidPosition(String actionName, Node position) {
        taskActions.clear();
        doExecute();
    }

    @Override
    protected void handleMoveFailure(MoveException.Problem cause, Node position) {
        taskActions.clear();
        doExecute();
    }

    @Override
    protected void handleMoveIntervalFailure(Node position) {
        doExecute();
    }

    @Override
    protected void handleContestEnded(String actionName) {
        // do nothing
    }

    @Override
    protected void handleContestNotStarted(String actionName) {
        // do nothing
    }

    public RoutePlanner getPlanner() {
        return planner;
    }

    public void setPlanner(RoutePlanner planner) {
        this.planner = planner;
    }
}
